﻿using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;

namespace GlobalHooksTest
{
    /// <summary>
    /// Generates recurring events. Able to wake up computer from Sleep/Hibernate.
    /// </summary>
    public class WaitableTimer: IDisposable{
        public class ElapsedEventArgs: EventArgs {
            private readonly DateTime signalTime;

            public ElapsedEventArgs(DateTime time) {
                signalTime = time;
            }

            public DateTime SignalTime {
                get { return signalTime; }
            }
        }
        public delegate void ElapsedEventHandler(object sender, ElapsedEventArgs e);

        private IntPtr hTimer;
        private double interval;
        private bool enabled;
        private string timerName;
        private bool disposed;

        /// <summary>
        /// If true (default), the <see cref="WaitableTimer"/>
        /// will resume computer from suspended (sleep, hibernate) state.
        /// </summary>
        [DefaultValue(true)]
        public bool ResumeSuspended { get; set;}

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitableTimer"/> class,
        /// and sets all the properties to their initial values.
        /// </summary>
        public WaitableTimer()
            : this(100) {
            }

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitableTimer"/> class,
        /// and sets the <see cref="WaitableTimer.Interval"/> property
        /// to the specified time period.
        /// </summary>
        /// <param name="interval">The time, in milliseconds, between events.</param>
        /// <exception cref="System.ArgumentException">
        /// The value of the interval parameter is less than or equal to zero
        /// </exception>
        public WaitableTimer(double interval) {
            ResumeSuspended = true;
            enabled = false;
            Interval = interval;
            AutoReset = true;
            hTimer = IntPtr.Zero;
        }

        ~WaitableTimer() {
            Dispose(false);
        }

        /// <summary>
        /// Gets or sets a value indicating whether the <see cref="WaitableTimer"/> should raise
        /// the <see cref="Elapsed"/> event.
        /// </summary>
        /// <value>
        /// true if the <see cref="WaitableTimer"/> should raise
        /// the <see cref="Elapsed"/> event;
        /// otherwise, false. The default is false.
        /// </value>
        /// <exception cref="System.ObjectDisposedException">
        /// This property cannot be set because the timer has been disposed.
        /// </exception>
        /// <exception cref="System.ComponentModel.Win32Exception">
        /// Unable to create timer handle.
        /// </exception>
        [DefaultValue(false)]
        public bool Enabled {
            get { return enabled; }
            set {
                if(enabled != value) {
                    enabled = value;
                    if(value){
                        if(hTimer == IntPtr.Zero) {
                            if(disposed) {
                                throw new System.ObjectDisposedException(GetType().Name);
                            }
                            int counter = 0;
                            timerName = GetType().Name + counter;
                            hTimer = WindowsHelper.WindowsAPI.CreateWaitableTimer(IntPtr.Zero, true, timerName);
                            int lastError = 0;
                            while(hTimer == IntPtr.Zero &&
                                  (lastError = Marshal.GetLastWin32Error()) == WindowsHelper.WindowsAPI.ERROR_ALREADY_EXISTS)
                            {
                                      counter++;
                                      timerName = GetType().Name + counter;
                                      hTimer = WindowsHelper.WindowsAPI.CreateWaitableTimer(IntPtr.Zero, true, timerName);
                                  }
                            if(hTimer == IntPtr.Zero) {
                                throw new Win32Exception(lastError);
                            }
                        }
                        ThreadPool.QueueUserWorkItem(SetTimerThread, this);
                        //instead of SetTimerThread(this), so user interface is not frozen,
                        //allow other threads to execute.
                    } else {
                        if(hTimer != IntPtr.Zero) {
                            WindowsHelper.WindowsAPI.CancelWaitableTimer(hTimer);
                            Close();
                        }
                    }
                }
            }
        }

        private static void SetTimerThread(object state) {
            var tmr = (WaitableTimer)state;
            var delay = (long)(-tmr.Interval * 10000);
            WindowsHelper.WindowsAPI.TimerAPCProc del = tmr.TimerTrigger;
            if (!WindowsHelper.WindowsAPI.SetWaitableTimer(
                    tmr.hTimer,
                    ref delay,
                    //0 below means that timer fires only once
                    //while AutoReset property is checked in TimerTrigger().
                    //This is done because the size of lPeriod parameter of SetWaitableTimer
                    //is smaller (Int32 as opposed to Int64) than Interval property
                    0, //tmr.AutoReset ? (int)tmr.Interval : 0,
                    del, //ptr,
                    IntPtr.Zero,
                    tmr.ResumeSuspended)) {
                        //throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
            uint ret = WindowsHelper.WindowsAPI.SleepEx(WindowsHelper.WindowsAPI.INFINITE, true);
        }

        private void TimerTrigger(
            IntPtr lpArgToCompletionRoutine,
            UInt32 dwTimerLowValue,
            UInt32 dwTimerHighValue) {
            enabled = false;
            try {
                ThreadPool.QueueUserWorkItem(TimerTriggerCallback, this);
                //instead of: TimerTriggerCallback(this);
                //so the timer is not stopped while Elapsed events are executing
                //Otherwise if MessageBox is shown in elapsed event,
                //autoresetting will only happen when this Messagebox is closed
            } finally {
                Enabled = AutoReset;
            }
            }

        private static void TimerTriggerCallback(object state) {
            if(state is WaitableTimer) {
                var tmr = (WaitableTimer)state;
                ElapsedEventHandler elapsed = tmr.Elapsed;
                if(elapsed != null) {
                    var e = new ElapsedEventArgs(DateTime.Now);
                    if(tmr.SynchronizingObject != null && tmr.SynchronizingObject.InvokeRequired) {
                        tmr.SynchronizingObject.BeginInvoke(elapsed, new object[] { tmr, e });
                    } else {
                        elapsed(tmr, e);
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the interval, in milliseconds, at which to raise 
        /// the <see cref="Elapsed"/> event.
        /// </summary>
        /// <exception cref="System.ArgumentException">
        /// The interval is less than or equal to zero.
        /// </exception>
        /// <remarks>
        /// The time, in milliseconds, between raisings of 
        /// the <see cref="Elapsed"/> event.
        /// The default is 100 milliseconds.
        /// </remarks>
        [DefaultValue(100)]
        public double Interval {
            get { return interval; }
            set {
                if(value <= 0) {
                    throw new ArgumentException("The interval is less than or equal to zero");
                }
                interval = value;
                if(Enabled) {
                    Stop();
                    Start();
                }
            }
        }

        /// <summary>
        /// Occurs when the interval elapses.
        /// </summary>
        public event ElapsedEventHandler Elapsed;

        /// <summary>
        /// Gets or sets a value indicating whether the <see cref="WaitableTimer"/> should raise
        /// the <see cref="Elapsed"/> event each time the specified interval elapses
        /// or only after the first time it elapses.
        /// </summary>
        /// <value>
        /// true if the <see cref="WaitableTimer"/> should raise the <see cref="Elapsed"/>
        /// event each time the interval elapses;
        /// false if it should raise the <see cref="Elapsed"/>
        /// event only once, after the first time the interval elapses.
        /// The default is true.
        /// </value>
        [DefaultValue(true)]
        public bool AutoReset { get; set; }
        
        /// <summary>
        /// Starts raising the <see cref="Elapsed"/> event
        /// by setting <see cref="Enabled"/> to true.
        /// </summary>
        public void Start() {
            Enabled = true;
        }

        /// <summary>
        ///Stops raising the <see cref="Elapsed"/> event
        ///by setting <see cref="WaitableTimer.Enabled"/> to false.
        /// </summary>
        public void Stop() {
            Enabled = false;
        }

        /// <summary>
        /// Gets or sets the object used to marshal event-handler calls that are issued
        /// when an interval has elapsed.
        /// </summary>
        /// <value>
        /// The System.ComponentModel.ISynchronizeInvoke representing the object used
        /// to marshal the event-handler calls that are issued when an interval has elapsed.
        /// The default is null.
        /// </value>
        [DefaultValue(null)]
        public System.ComponentModel.ISynchronizeInvoke SynchronizingObject { get; set; }

        /// <summary>
        /// Releases the resources used by the <see cref="WaitableTimer"/>.
        /// </summary>
        protected void Close() {
            if(hTimer != IntPtr.Zero) {
                WindowsHelper.WindowsAPI.CloseHandle(hTimer);
                hTimer = IntPtr.Zero;
            }
        }

        #region IDisposable Members
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion

        protected void Dispose(bool disposing) {
            if(!disposed) {
                Close();
                disposed = true;
            }
        }
    }
}